在日常业务开发中,我们经常遇到一些需要定时执行的任务,比如定时给用户发短信发邮件,定时检查某个指标或参数是否达标等。

Spring Boot 中的定时任务

创建定时任务

在Spring Boot应用中创建定时任务非常简单。只要以下两步就可以:

  1. DemoApplication类上加上@EnableScheduling开启定时任务配置:
1
2
3
4
5
6
7
8
9
@SpringBootApplication
...
@EnableScheduling
public class DemoApplication {

public static void main(String[] args) {
SpringApplication.run(DemoApplication.class, args);
}
}
  1. 创建定时任务类,在方法上添加@Scheduled注解使方法定时执行:
1
2
3
4
5
6
7
8
9
10
11
12
@Component
@Slf4j
public class TestTask {

private static final SimpleDateFormat dateFormat = new SimpleDateFormat("HH:mm:ss");

//每5秒执行
@Scheduled(fixedRate = 5000)
public void reportCurrentTime() {
log.info("The time is now {}", dateFormat.format(new Date()));
}
}
  1. 运行程序,查看输出:
1
2
3
4
5
6
7
8
...
2018-04-09 14:43:08.631 INFO 13273 --- [ main] o.s.b.w.embedded.tomcat.TomcatWebServer : Tomcat started on port(s): 8080 (http) with context path ''
2018-04-09 14:43:08.636 INFO 13273 --- [ main] com.tt.study.demo.DemoApplication : Started DemoApplication in 7.855 seconds (JVM running for 8.709)
2018-04-09 14:43:13.608 INFO 13273 --- [pool-5-thread-1] c.t.s.demo.service.schedule.TestTask : The time is now 14:43:13
2018-04-09 14:43:18.609 INFO 13273 --- [pool-5-thread-1] c.t.s.demo.service.schedule.TestTask : The time is now 14:43:18
2018-04-09 14:43:23.609 INFO 13273 --- [pool-5-thread-1] c.t.s.demo.service.schedule.TestTask : The time is now 14:43:23
2018-04-09 14:43:28.608 INFO 13273 --- [pool-5-thread-1] c.t.s.demo.service.schedule.TestTask : The time is now 14:43:28
2018-04-09 14:43:33.608 INFO 13273 --- [pool-5-thread-1] c.t.s.demo.service.schedule.TestTask : The time is now 14:43:33

@Scheduled用法详解

  • @Scheduled(fixedDelay=5000)在上次执行完成后间隔5秒再次执行。
  • @Scheduled(fixedRate=5000)固定间隔5秒执行。
  • @Scheduled(initialDelay=1000, fixedRate=5000)第一次延迟1秒后执行,之后固定间隔5秒执行。
  • @Scheduled(cron="*/5 * * * * MON-FRI")使用cron表达式,周一到周五,固定间隔5秒执行。

问题

我们在单机使用时,任务能定时有效的运行。但是当我们部署服务器集群后,会出现任务多次调用的情况,因为集群之间不会共享任务调用信息,每个节点上的任务都会执行。

一个比较简单的解决方案,就是给定时任务加上锁,只有获取到锁的时候才能执行。我们可以简单的用redis分布式锁来实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
@Component
@Slf4j
public class TestTask {

private static final SimpleDateFormat dateFormat = new SimpleDateFormat("HH:mm:ss");
@Autowired
private RedisTemplate<String, Object> redisTemplate;

@Scheduled(fixedRate = 5000)
public void reportCurrentTime() {
//集群部署时需要加锁
String key = "task";
if (!redisTemplate.opsForValue().setIfAbsent(key, 1)) {
return;
}
//设置key过期
redisTemplate.expire(key, 3, TimeUnit.SECONDS);
//执行任务内容
log.info("The time is now {}", dateFormat.format(new Date()));
}
}

只有获取到锁的那台才会执行具体任务。注意key的过期时间需要小于两次执行间隔,大于集群服务器之间的时间差。

这里会有一个问题,如果服务器刚好运行到设置key过期这一步挂了,那么如果不去手动删掉这个key,这个定时就不会再执行了。(虽然这种事件出现概率极小)

当然,你也可以使用一些开源的分布式任务调度框架,如:XXL-JOBElastic-Job等。我们之后再说